]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
extensions-app: Split out ExtensionManager
authorFlorian Müllner <fmuellner@gnome.org>
Mon, 18 Dec 2023 19:20:26 +0000 (20:20 +0100)
committerMarge Bot <marge-bot@gnome.org>
Thu, 21 Dec 2023 15:24:23 +0000 (15:24 +0000)
The extension handling is currently intertwined with the UI.

Splitting it out provides a clearer separation, and will allow
us to switch to a model-based UI later.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3067>

po/POTFILES.in
subprojects/extensions-app/js/extensionManager.js [new file with mode: 0644]
subprojects/extensions-app/js/extensionRow.js
subprojects/extensions-app/js/extensionsWindow.js
subprojects/extensions-app/js/main.js
subprojects/extensions-app/js/org.gnome.Extensions.src.gresource.xml.in

index f7c05eefc85058458da5b1809b3c2e7c290b486f..35cb77c6969631f336f0c989ea3774d06e1a29b7 100644 (file)
@@ -92,6 +92,7 @@ src/shell-util.c
 src/st/st-icon-theme.c
 subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in
 subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in
+subprojects/extensions-app/js/extensionManager.js
 subprojects/extensions-app/js/extensionRow.js
 subprojects/extensions-app/js/extensionsWindow.js
 subprojects/extensions-app/data/ui/extension-row.ui
diff --git a/subprojects/extensions-app/js/extensionManager.js b/subprojects/extensions-app/js/extensionManager.js
new file mode 100644 (file)
index 0000000..25f0261
--- /dev/null
@@ -0,0 +1,295 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+
+import {
+    ExtensionState, ExtensionType, deserializeExtension
+}  from './misc/extensionUtils.js';
+
+const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
+const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
+
+let shellVersion;
+
+function loadInterfaceXML(iface) {
+    const uri = `resource:///org/gnome/Extensions/dbus-interfaces/${iface}.xml`;
+    const f = Gio.File.new_for_uri(uri);
+
+    try {
+        let [ok_, bytes] = f.load_contents(null);
+        return new TextDecoder().decode(bytes);
+    } catch (e) {
+        console.error(`Failed to load D-Bus interface ${iface}`);
+    }
+
+    return null;
+}
+
+class Extension {
+    constructor(variant) {
+        this.update(variant);
+    }
+
+    update(variant) {
+        const deserialized = deserializeExtension(variant);
+
+        const {
+            uuid, type, state, error, hasPrefs, hasUpdate, canChange, metadata,
+        } = deserialized;
+
+        if (!this._uuid)
+            this._uuid = uuid;
+
+        if (this._uuid !== uuid)
+            throw new Error(`Invalid update of extension ${this._uuid} with data from ${uuid}`);
+
+        const {name} = metadata;
+        this._name = name;
+        [this._keywords] = GLib.str_tokenize_and_fold(name, null);
+
+        const [desc] = metadata.description.split('\n');
+        this._description = desc;
+
+        this._type = type;
+        this._errorDetail = error;
+        this._state = state;
+
+        const creator = metadata.creator ?? '';
+        this._creator = creator;
+
+        const url = metadata.url ?? '';
+        this._url = url;
+
+        const version = String(
+            metadata['version-name'] || metadata['version'] || '');
+        this._version = version;
+
+        this._hasPrefs = hasPrefs;
+        this._hasUpdate = hasUpdate;
+        this._canChange = canChange;
+    }
+
+    get uuid() {
+        return this._uuid;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get description() {
+        return this._description;
+    }
+
+    get state() {
+        return this._state;
+    }
+
+    get creator() {
+        return this._creator;
+    }
+
+    get url() {
+        return this._url;
+    }
+
+    get version() {
+        return this._version;
+    }
+
+    get keywords() {
+        return this._keywords;
+    }
+
+    get error() {
+        if (!this.hasError)
+            return '';
+
+        if (this.state === ExtensionState.OUT_OF_DATE) {
+            return this.version !== ''
+                ? _('The installed version of this extension (%s) is incompatible with the current version of GNOME (%s). The extension has been disabled.').format(this.version, shellVersion)
+                : _('The installed version of this extension is incompatible with the current version of GNOME (%s). The extension has been disabled.').format(shellVersion);
+        }
+
+        const message = [
+            _('An error has occurred in this extension. This could cause issues elsewhere in the system. It is recommended to turn the extension off until the error is resolved.'),
+        ];
+
+        if (this._errorDetail) {
+            message.push(
+                // translators: Details for an extension error
+                _('Error details:'), this._errorDetail);
+        }
+
+        return message.join('\n\n');
+    }
+
+    get hasError() {
+        return this.state === ExtensionState.OUT_OF_DATE ||
+               this.state === ExtensionState.ERROR;
+    }
+
+    get hasPrefs() {
+        return this._hasPrefs;
+    }
+
+    get hasUpdate() {
+        return this._hasUpdate;
+    }
+
+    get hasVersion() {
+        return this._version !== '';
+    }
+
+    get canChange() {
+        return this._canChange;
+    }
+
+    get isUser() {
+        return this._type === ExtensionType.PER_USER;
+    }
+}
+
+export const ExtensionManager = GObject.registerClass({
+    Properties: {
+        'user-extensions-enabled': GObject.ParamSpec.boolean(
+            'user-extensions-enabled', null, null,
+            GObject.ParamFlags.READWRITE,
+            true),
+        'n-updates': GObject.ParamSpec.int(
+            'n-updates', null, null,
+            GObject.ParamFlags.READABLE,
+            0, 999, 0),
+        'failed': GObject.ParamSpec.boolean(
+            'failed', null, null,
+            GObject.ParamFlags.READABLE,
+            false),
+    },
+    Signals: {
+        'extension-added': {param_types: [GObject.TYPE_JSOBJECT]},
+        'extension-removed': {param_types: [GObject.TYPE_JSOBJECT]},
+        'extension-changed': {param_types: [GObject.TYPE_JSOBJECT], flags: GObject.SignalFlags.DETAILED},
+        'extensions-loaded': {},
+    },
+}, class ExtensionManager extends GObject.Object {
+    constructor() {
+        super();
+
+        this._extensions = new Map();
+
+        this._proxyReady = false;
+        this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
+            'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions',
+            () => {
+                this._proxyReady = true;
+                shellVersion = this._shellProxy.ShellVersion;
+
+                this._shellProxy.connect('notify::g-name-owner',
+                    () => this.notify('failed'));
+                this.notify('failed');
+            });
+
+        this._shellProxy.connect('g-properties-changed', (proxy, properties) => {
+            const enabledChanged = !!properties.lookup_value('UserExtensionsEnabled', null);
+            if (enabledChanged)
+                this.notify('user-extensions-enabled');
+        });
+        this._shellProxy.connectSignal(
+            'ExtensionStateChanged', this._onExtensionStateChanged.bind(this));
+
+        this._loadExtensions().catch(console.error);
+    }
+
+    get userExtensionsEnabled() {
+        return this._shellProxy.UserExtensionsEnabled ?? false;
+    }
+
+    set userExtensionsEnabled(enabled) {
+        this._shellProxy.UserExtensionsEnabled = enabled;
+    }
+
+    get nUpdates() {
+        let nUpdates = 0;
+        for (const ext of this._extensions.values()) {
+            if (ext.isUser && ext.hasUpdate)
+                nUpdates++;
+        }
+        return nUpdates;
+    }
+
+    get failed() {
+        return this._proxyReady && this._shellProxy.gNameOwner === null;
+    }
+
+    enableExtension(uuid) {
+        this._shellProxy.EnableExtensionAsync(uuid).catch(console.error);
+    }
+
+    disableExtension(uuid) {
+        this._shellProxy.DisableExtensionAsync(uuid).catch(console.error);
+    }
+
+    uninstallExtension(uuid) {
+        this._shellProxy.UninstallExtensionAsync(uuid).catch(console.error);
+    }
+
+    openExtensionPrefs(uuid, parentHandle) {
+        this._shellProxy.OpenExtensionPrefsAsync(uuid,
+            parentHandle,
+            {modal: new GLib.Variant('b', true)}).catch(console.error);
+    }
+
+    checkForUpdates() {
+        this._shellProxy.CheckForUpdatesAsync().catch(console.error);
+    }
+
+    _addExtension(extension) {
+        const {uuid} = extension;
+        if (this._extensions.has(uuid))
+            return;
+
+        this._extensions.set(uuid, extension);
+        this.emit('extension-added', extension);
+    }
+
+    _removeExtension(extension) {
+        const {uuid} = extension;
+        if (this._extensions.delete(uuid))
+            this.emit('extension-removed', extension);
+    }
+
+    async _loadExtensions() {
+        const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
+
+        for (let uuid in extensionsMap) {
+            const extension = new Extension(extensionsMap[uuid]);
+            this._addExtension(extension);
+        }
+        this.emit('extensions-loaded');
+    }
+
+    _onExtensionStateChanged(p, sender, [uuid, newState]) {
+        const extension = this._extensions.get(uuid);
+
+        if (extension)
+            extension.update(newState);
+
+        if (!extension)
+            this._addExtension(new Extension(newState));
+        else if (extension.state === ExtensionState.UNINSTALLED)
+            this._removeExtension(extension);
+        else
+            this.emit(`extension-changed::${uuid}`, extension);
+
+        if (this._updatesCheckId)
+            return;
+
+        this._updatesCheckId = GLib.timeout_add_seconds(
+            GLib.PRIORITY_DEFAULT, 1, () => {
+                this.notify('n-updates');
+
+                delete this._updatesCheckId;
+                return GLib.SOURCE_REMOVE;
+            });
+    }
+});
index 23bed79d2435071bb1848b60b0881c610bcb5865..d2bb2c4981bc72370fc0043ffd1aad0daf0feb23 100644 (file)
@@ -3,7 +3,7 @@ import GLib from 'gi://GLib';
 import Gio from 'gi://Gio';
 import GObject from 'gi://GObject';
 
-import {ExtensionState, ExtensionType, deserializeExtension}  from './misc/extensionUtils.js';
+import {ExtensionState}  from './misc/extensionUtils.js';
 
 export const ExtensionRow = GObject.registerClass({
     GTypeName: 'ExtensionRow',
@@ -25,40 +25,38 @@ export const ExtensionRow = GObject.registerClass({
         this._app = Gio.Application.get_default();
         this._extension = extension;
 
-        [this._keywords] = GLib.str_tokenize_and_fold(this.name, null);
-
         this._actionGroup = new Gio.SimpleActionGroup();
         this.insert_action_group('row', this._actionGroup);
 
         let action;
         action = new Gio.SimpleAction({
             name: 'show-prefs',
-            enabled: this.hasPrefs,
+            enabled: extension.hasPrefs,
         });
         action.connect('activate', () => {
             this._detailsPopover.popdown();
-            this.get_root().openPrefs(this.uuid);
+            this.get_root().openPrefs(extension);
         });
         this._actionGroup.add_action(action);
 
         action = new Gio.SimpleAction({
             name: 'show-url',
-            enabled: this.url !== '',
+            enabled: extension.url !== '',
         });
         action.connect('activate', () => {
             this._detailsPopover.popdown();
             Gio.AppInfo.launch_default_for_uri(
-                this.url, this.get_display().get_app_launch_context());
+                extension.url, this.get_display().get_app_launch_context());
         });
         this._actionGroup.add_action(action);
 
         action = new Gio.SimpleAction({
             name: 'uninstall',
-            enabled: this.type === ExtensionType.PER_USER,
+            enabled: extension.isUser,
         });
         action.connect('activate', () => {
             this._detailsPopover.popdown();
-            this.get_root().uninstall(this.uuid);
+            this.get_root().uninstall(extension);
         });
         this._actionGroup.add_action(action);
 
@@ -70,97 +68,28 @@ export const ExtensionRow = GObject.registerClass({
             const state = action.get_state();
             action.change_state(new GLib.Variant('b', !state.get_boolean()));
         });
-
         action.connect('change-state', (a, state) => {
+            const {uuid} = this._extension;
             if (state.get_boolean())
-                this._app.shellProxy.EnableExtensionAsync(this.uuid).catch(console.error);
+                this._app.extensionManager.enableExtension(uuid);
             else
-                this._app.shellProxy.DisableExtensionAsync(this.uuid).catch(console.error);
+                this._app.extensionManager.disableExtension(uuid);
         });
         this._actionGroup.add_action(action);
 
-        this.title = this.name;
+        this.title = extension.name;
 
-        const desc = this._extension.metadata.description.split('\n')[0];
-        this._descriptionLabel.label = desc;
+        this._descriptionLabel.label = extension.description;
 
         this.connect('destroy', this._onDestroy.bind(this));
 
-        this._extensionStateChangedId = this._app.shellProxy.connectSignal(
-            'ExtensionStateChanged', (p, sender, [uuid, newState]) => {
-                if (this.uuid !== uuid)
-                    return;
-
-                this._extension = deserializeExtension(newState);
-                this._updateState();
-            });
+        this._extensionStateChangedId = this._app.extensionManager.connect(
+            `extension-changed::${extension.uuid}`, () => this._updateState());
         this._updateState();
     }
 
-    get uuid() {
-        return this._extension.uuid;
-    }
-
-    get name() {
-        return this._extension.metadata.name;
-    }
-
-    get hasPrefs() {
-        return this._extension.hasPrefs;
-    }
-
-    get hasUpdate() {
-        return this._extension.hasUpdate || false;
-    }
-
-    get hasError() {
-        const {state} = this._extension;
-        return state === ExtensionState.OUT_OF_DATE ||
-               state === ExtensionState.ERROR;
-    }
-
-    get type() {
-        return this._extension.type;
-    }
-
-    get creator() {
-        return this._extension.metadata.creator || '';
-    }
-
-    get url() {
-        return this._extension.metadata.url || '';
-    }
-
-    get version() {
-        return this._extension.metadata['version-name'] || this._extension.metadata.version || '';
-    }
-
-    get error() {
-        if (!this.hasError)
-            return '';
-
-        if (this._extension.state === ExtensionState.OUT_OF_DATE) {
-            const {ShellVersion: shellVersion} = this._app.shellProxy;
-            return this.version !== ''
-                ? _('The installed version of this extension (%s) is incompatible with the current version of GNOME (%s). The extension has been disabled.').format(this.version, shellVersion)
-                : _('The installed version of this extension is incompatible with the current version of GNOME (%s). The extension has been disabled.').format(shellVersion);
-        }
-
-        const message = [
-            _('An error has occurred in this extension. This could cause issues elsewhere in the system. It is recommended to turn the extension off until the error is resolved.'),
-        ];
-
-        if (this._extension.error) {
-            message.push(
-                // translators: Details for an extension error
-                _('Error details:'), this._extension.error);
-        }
-
-        return message.join('\n\n');
-    }
-
-    get keywords() {
-        return this._keywords;
+    get extension() {
+        return this._extension;
     }
 
     _updateState() {
@@ -173,21 +102,17 @@ export const ExtensionRow = GObject.registerClass({
         if (!action.enabled)
             this._switch.active = state;
 
-        this._updatesButton.visible = this.hasUpdate;
-        this._errorButton.visible = this.hasError;
-        this._errorLabel.label = this.error;
+        this._updatesButton.visible = this._extension.hasUpdate;
+        this._errorButton.visible = this._extension.hasError;
+        this._errorLabel.label = this._extension.error;
 
-        this._versionLabel.label = _('Version %s').format(this.version.toString());
-        this._versionLabel.visible = this.version !== '';
+        this._versionLabel.label = _('Version %s').format(this._extension.version);
+        this._versionLabel.visible = this._extension.version !== '';
     }
 
     _onDestroy() {
-        if (!this._app.shellProxy)
-            return;
-
         if (this._extensionStateChangedId)
-            this._app.shellProxy.disconnectSignal(this._extensionStateChangedId);
-        this._extensionStateChangedId = 0;
+            this._app.extensionManager.disconnect(this._extensionStateChangedId);
+        delete this._extensionStateChangedId;
     }
 });
-
index 66671a68f5211cf44681a74937d55d86b476fc53..4af3630ea93c3be5b8729ce546a24b0fc486bd9a 100644 (file)
@@ -10,7 +10,6 @@ import * as Gettext from 'gettext';
 
 import * as Config from './misc/config.js';
 import {ExtensionRow} from './extensionRow.js';
-import {ExtensionState, ExtensionType, deserializeExtension}  from './misc/extensionUtils.js';
 
 Gio._promisify(Gio.DBusConnection.prototype, 'call');
 Gio._promisify(Shew.WindowExporter.prototype, 'export');
@@ -36,8 +35,6 @@ export const ExtensionsWindow = GObject.registerClass({
         if (Config.PROFILE === 'development')
             this.add_css_class('devel');
 
-        this._updatesCheckId = 0;
-
         this._exporter = new Shew.WindowExporter({window: this});
         this._exportedHandle = '';
 
@@ -52,7 +49,8 @@ export const ExtensionsWindow = GObject.registerClass({
                 name: 'user-extensions-enabled',
                 state: 'false',
                 change_state: (a, state) => {
-                    this._shellProxy.UserExtensionsEnabled = state.get_boolean();
+                    const {extensionManager} = this.application;
+                    extensionManager.userExtensionsEnabled = state.get_boolean();
                 },
             }]);
 
@@ -90,27 +88,38 @@ export const ExtensionsWindow = GObject.registerClass({
         }));
         this._systemList.connect('row-activated', (_list, row) => row.activate());
 
-        this._shellProxy.connectSignal('ExtensionStateChanged',
-            this._onExtensionStateChanged.bind(this));
-
-        this._shellProxy.connect('g-properties-changed',
+        const {extensionManager} = this.application;
+        extensionManager.connect('notify::failed',
+            () => this._syncVisiblePage());
+        extensionManager.connect('notify::n-updates',
+            () => this._checkUpdates());
+        extensionManager.connect('notify::user-extensions-enabled',
             this._onUserExtensionsEnabledChanged.bind(this));
         this._onUserExtensionsEnabledChanged();
 
-        this._scanExtensions();
-    }
+        extensionManager.connect('extension-added',
+            (mgr, extension) => this._addExtensionRow(extension));
+        extensionManager.connect('extension-removed',
+            (mgr, extension) => this._removeExtensionRow(extension));
+        extensionManager.connect('extension-changed',
+            (mgr, extension) => {
+                const row = this._findExtensionRow(extension);
+                const isUser = row?.get_parent() === this._userList;
+                if (extension.isUser !== isUser) {
+                    this._removeExtensionRow(extension);
+                    this._addExtensionRow(extension);
+                }
+            });
 
-    get _shellProxy() {
-        return this.application.shellProxy;
+        extensionManager.connect('extensions-loaded',
+            () => this._extensionsLoaded());
     }
 
-    uninstall(uuid) {
-        const row = this._findExtensionRow(uuid);
-
+    uninstall(extension) {
         const dialog = new Gtk.MessageDialog({
             transient_for: this,
             modal: true,
-            text: _('Remove “%s”?').format(row.name),
+            text: _('Remove “%s”?').format(extension.name),
             secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'),
         });
 
@@ -119,14 +128,16 @@ export const ExtensionsWindow = GObject.registerClass({
             .get_style_context().add_class('destructive-action');
 
         dialog.connect('response', (dlg, response) => {
+            const {extensionManager} = this.application;
+
             if (response === Gtk.ResponseType.ACCEPT)
-                this._shellProxy.UninstallExtensionAsync(uuid).catch(console.error);
+                extensionManager.uninstallExtension(extension.uuid);
             dialog.destroy();
         });
         dialog.present();
     }
 
-    async openPrefs(uuid) {
+    async openPrefs(extension) {
         if (!this._exportedHandle) {
             try {
                 this._exportedHandle = await this._exporter.export();
@@ -135,9 +146,8 @@ export const ExtensionsWindow = GObject.registerClass({
             }
         }
 
-        this._shellProxy.OpenExtensionPrefsAsync(uuid,
-            this._exportedHandle,
-            {modal: new GLib.Variant('b', true)}).catch(console.error);
+        const {extensionManager} = this.application;
+        extensionManager.openExtensionPrefs(extension.uuid, this._exportedHandle);
     }
 
     _showAbout() {
@@ -180,104 +190,68 @@ export const ExtensionsWindow = GObject.registerClass({
     }
 
     _sortList(row1, row2) {
-        return row1.name.localeCompare(row2.name);
+        const {name: name1} = row1.extension;
+        const {name: name2} = row2.extension;
+        return name1.localeCompare(name2);
     }
 
     _filterList(row) {
+        const {keywords} = row.extension;
         return this._searchTerms.every(
-            t => row.keywords.some(k => k.startsWith(t)));
+            t => keywords.some(k => k.startsWith(t)));
     }
 
-    _findExtensionRow(uuid) {
+    _findExtensionRow(extension) {
         return [
             ...this._userList,
             ...this._systemList,
-        ].find(c => c.uuid === uuid);
+        ].find(c => c.extension === extension);
     }
 
     _onUserExtensionsEnabledChanged() {
+        const {userExtensionsEnabled} = this.application.extensionManager;
         const action = this.lookup_action('user-extensions-enabled');
-        action.set_state(
-            new GLib.Variant('b', this._shellProxy.UserExtensionsEnabled));
-    }
-
-    _onExtensionStateChanged(proxy, senderName, [uuid, newState]) {
-        const extension = deserializeExtension(newState);
-        let row = this._findExtensionRow(uuid);
-
-        this._queueUpdatesCheck();
-
-        // the extension's type changed; remove the corresponding row
-        // and reset the variable to null so that we create a new row
-        // below and add it to the appropriate list
-        if (row && row.type !== extension.type) {
-            row.get_parent().remove(row);
-            row = null;
-        }
-
-        if (row) {
-            if (extension.state === ExtensionState.UNINSTALLED)
-                row.get_parent().remove(row);
-        } else {
-            this._addExtensionRow(extension);
-        }
-
-        this._syncListVisibility();
-    }
-
-    async _scanExtensions() {
-        try {
-            const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
-
-            for (let uuid in extensionsMap) {
-                const extension = deserializeExtension(extensionsMap[uuid]);
-                this._addExtensionRow(extension);
-            }
-            this._extensionsLoaded();
-        } catch (e) {
-            if (e instanceof Gio.DBusError) {
-                console.log(`Failed to connect to shell proxy: ${e}`);
-                this._mainStack.visible_child_name = 'noshell';
-            } else {
-                throw e;
-            }
-        }
+        action.set_state(new GLib.Variant('b', userExtensionsEnabled));
     }
 
     _addExtensionRow(extension) {
         const row = new ExtensionRow(extension);
 
-        if (row.type === ExtensionType.PER_USER)
+        if (extension.isUser)
             this._userList.append(row);
         else
             this._systemList.append(row);
-    }
-
-    _queueUpdatesCheck() {
-        if (this._updatesCheckId)
-            return;
 
-        this._updatesCheckId = GLib.timeout_add_seconds(
-            GLib.PRIORITY_DEFAULT, 1, () => {
-                this._checkUpdates();
+        this._syncListVisibility();
+    }
 
-                this._updatesCheckId = 0;
-                return GLib.SOURCE_REMOVE;
-            });
+    _removeExtensionRow(extension) {
+        const row = this._findExtensionRow(extension);
+        if (row)
+            row.get_parent().remove(row);
+        this._syncListVisibility();
     }
 
     _syncListVisibility() {
         this._userGroup.visible = [...this._userList].length > 1;
         this._systemGroup.visible = [...this._systemList].length > 1;
 
-        if (this._userGroup.visible || this._systemGroup.visible)
+        this._syncVisiblePage();
+    }
+
+    _syncVisiblePage() {
+        const {extensionManager} = this.application;
+
+        if (extensionManager.failed)
+            this._mainStack.visible_child_name = 'noshell';
+        else if (this._userGroup.visible || this._systemGroup.visible)
             this._mainStack.visible_child_name = 'main';
         else
             this._mainStack.visible_child_name = 'placeholder';
     }
 
     _checkUpdates() {
-        const nUpdates = [...this._userList].filter(c => c.hasUpdate).length;
+        const {nUpdates} = this.application.extensionManager;
 
         this._updatesBanner.title = Gettext.ngettext(
             '%d extension will be updated on next login.',
index 47f0d061ad34bfd070384e8c79df4ea4909a5630..7b3af1cc73f2dd8fcfd266602eb410ac01cbaa32 100644 (file)
@@ -1,6 +1,5 @@
 import Adw from 'gi://Adw?version=1';
 import GLib from 'gi://GLib';
-import Gio from 'gi://Gio';
 import GObject from 'gi://GObject';
 
 import {setConsoleLogDomain} from 'console';
@@ -8,25 +7,9 @@ const Package = imports.package;
 
 Package.initFormat();
 
+import {ExtensionManager} from './extensionManager.js';
 import {ExtensionsWindow} from './extensionsWindow.js';
 
-const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
-const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
-
-function loadInterfaceXML(iface) {
-    const uri = `resource:///org/gnome/Extensions/dbus-interfaces/${iface}.xml`;
-    const f = Gio.File.new_for_uri(uri);
-
-    try {
-        let [ok_, bytes] = f.load_contents(null);
-        return new TextDecoder().decode(bytes);
-    } catch (e) {
-        console.error(`Failed to load D-Bus interface ${iface}`);
-    }
-
-    return null;
-}
-
 var Application = GObject.registerClass(
 class Application extends Adw.Application {
     _init() {
@@ -36,12 +19,12 @@ class Application extends Adw.Application {
         this.connect('window-removed', (a, window) => window.run_dispose());
     }
 
-    get shellProxy() {
-        return this._shellProxy;
+    get extensionManager() {
+        return this._extensionManager;
     }
 
     vfunc_activate() {
-        this._shellProxy.CheckForUpdatesAsync().catch(console.error);
+        this._extensionManager.checkForUpdates();
         this._window.present();
     }
 
@@ -56,8 +39,7 @@ class Application extends Adw.Application {
 
         this.set_accels_for_action('app.quit', ['<Primary>q']);
 
-        this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
-            'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions');
+        this._extensionManager = new ExtensionManager();
 
         this._window = new ExtensionsWindow({application: this});
     }
index eb7759aec6ad1396cd33aecfbf570d73ab6dc95d..554f3c77db407f4107a6cbcd516c5c06b49f4932 100644 (file)
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/Extensions@profile@/js">
+    <file>extensionManager.js</file>
     <file>extensionRow.js</file>
     <file>extensionsWindow.js</file>
     <file>main.js</file>